策略权限守卫:测试默认的策略权限的三种逻辑
在完成 PolicyGuard 和 CaslAbilityService 的基础实现后,需要对其三种核心权限逻辑进行全面验证:基础 Action/Subject 匹配、Fields 字段级控制和 Conditions 条件匹配。本节通过断点调试逐步验证每种逻辑的正确性,并解决 subject 实例转换的关键问题。
权限测试数据准备
创建接口关联的 Policy 数据
通过 Permission 的 PATCH 接口为特定路由创建关联的策略权限:
// PATCH /permissions/:id
{
"type": 0, // JSON 类型
"effect": "can",
"action": "read",
"subject": "user",
"conditions": {}
}
json
这相当于在数据库中创建了一条 Permission (id=21) 与 Policy (id=10) 的关联关系。
创建用户角色关联的 Policy 数据
通过更新角色接口,为用户分配具体的策略权限:
// PATCH /roles/:id
{
"permissions": [
{
"type": 0,
"effect": "can",
"action": "read",
"subject": "user"
}
]
}
json
NestJS 断点调试配置
在 VS Code 中创建 launch.json 文件,用于 NestJS 应用的调试:
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "NestJS Debug",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "start:dev"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"runtimeVersion": "20.11.1"
}
]
}
json
在 PolicyGuard 的关键位置设置断点:buildAbility 调用处(观察生成的 ability 实例)和权限判断逻辑处。
逻辑一:基础 Action/Subject 匹配
验证流程
发送请求后,程序进入 PolicyGuard,buildAbility 从数据库读取用户角色的 Policy 并创建 ability 实例:
// CaslAbilityService - buildAbility 内部
const abilities = []; // ability 实例数组
// 当用户只有 read user 权限时
// abilities.rules => [{ action: 'read', subject: 'user' }]
typescript
当接口要求 update user 权限,而用户只有 read user 权限时:
const permissionGranted = ability.can('update', userInstance);
// => false,因为用户只有 read 权限
typescript
结果:allPermissionsGranted = false,返回 403 Forbidden。
Subject 实例转换的关键问题
CASL 的 ability.can() 方法要求第二个参数必须是类实例(class instance),而非普通对象。直接传递数据库查询返回的纯对象会导致条件匹配失败:
// 错误:Prisma 返回的是普通对象,不是类实例
const userObj = await prisma.user.findUnique({ where: { id } });
ability.can('update', userObj); // 条件匹配可能不正确
typescript
需要通过 class-transformer 的 plainToInstance 进行转换,并建立 subject 到 class 的映射:
import { plainToInstance } from 'class-transformer';
import { User } from '../entities/user.entity';
const mapSubjectToClass = (subject: string) => {
const map = {
user: User,
// 其他 entity 映射...
};
return map[subject] || null;
};
// 在 PolicyGuard 中
const obj = /* Prisma 查询结果 */;
const subjectClass = mapSubjectToClass(subject);
const subjectInstance = typeof subjectClass === 'string'
? subject
: plainToInstance(subjectClass, obj);
typescript
转换后 ability.can('update', userInstance) 能正确识别 User 类实例并匹配条件。
逻辑二:Fields 字段级权限控制
接口配置字段要求
为接口配置需要特定字段权限的 Policy:
{
"type": 0,
"effect": "can",
"action": "update",
"subject": "user",
"fields": ["username", "password"]
}
json
场景测试对比
| 用户拥有权限 | 接口要求权限 | 结果 |
|---|---|---|
can update user (无字段限制) | update user fields: [username, password] | 通过 -- 无字段限制代表全部字段 |
can update user fields: [username] | update user fields: [username, password] | 403 -- 缺少 password 字段权限 |
字段验证的核心逻辑使用 Array.every() 确保用户拥有所有要求字段的权限:
// PolicyGuard 中的字段判断逻辑
if (policy.fields && policy.fields.length > 0) {
const permissionGranted = policy.fields.every(field =>
ability.can(action, subjectInstance, field)
);
if (!permissionGranted) {
tempPermissionPolicies.push(policy);
}
}
typescript
逻辑三:Conditions 条件匹配
静态条件匹配
为 Policy 添加 conditions 条件:
{
"type": 0,
"effect": "can",
"action": "update",
"subject": "user",
"conditions": { "username": "tom1" }
}
json
CASL 在判断权限时,会将 conditions 与 subject 实例的属性值进行匹配:
// ability 内部会检查 subjectInstance.username === 'tom1'
const rules = ability.rules;
// [{ action: 'update', subject: 'user', conditions: { username: 'tom1' } }]
const permissionGranted = ability.can('update', subjectInstance);
// subjectInstance.username === 'tom1' => true
// subjectInstance.username !== 'tom1' => false
typescript
三种逻辑的完整判断流程
请求进入 PolicyGuard
│
├─ 1. buildAbility() ── 根据 type 分发到不同 handler
│ ├─ type=0 → handleJsonType (基础条件)
│ ├─ type=1 → handleMongoType (MongoDB 查询)
│ └─ type=2 → handleFunctionType (函数条件)
│
├─ 2. 遍历接口要求的 Policy(左侧大循环)
│ └─ 对每个 requiredPolicy:
│ 遍历用户拥有的 ability(右侧大循环)
│ │
│ ├─ 检查 action 匹配
│ ├─ 检查 subject 匹配
│ ├─ 检查 fields 权限(如有)
│ └─ 检查 conditions 匹配(如有)
│
└─ 3. 若所有 requiredPolicy 都通过 → 200
否则 → 403 Forbidden
text
遗留问题
静态 conditions 无法满足动态场景需求。例如"用户只能更新自己的密码",需要将 conditions 中的值动态绑定到当前用户。解决方案是通过函数类型的 Policy 实现,将在下一节详细讲解。
↑